/**
 *  'Borrowed' from DOM.js in Firebug package.
 * See license.txt for terms of usage
 **/

FBL.ns(function()
{
    with ( FBL )
    {

        try
        {

            // ************************************************************************************************
            // Constants

            const jsdIStackFrame = CI("jsdIStackFrame");

            // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

            const INSERT_SLICE_SIZE = 18;
            const insertInterval = 40;

            const ignoreVars =
            {
                "__firebug__": 1,
                "eval": 1,

            // We are forced to ignore Java-related variables, because
            // trying to access them causes browser freeze
                "java": 1,
                "sun": 1,
                "Packages": 1
            };

            // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

            /**
             * Defines the Look and Feel of each row for the west Object selection tree panel.
             */
            const RowTag =
                TR({class: "$member.upDownRow memberRow $member.open $member.styleClassName\\Row", $hasChildren: "$member.hasChildren",
                    level: "$member.level", isTestFire: "$member.isTestFire", _associatedObject: "$member.associatedObject"},
                    TD({class: "memberLabelCell", style: "padding-left: $member.indent\\px"},
                        DIV({class: "memberLabel $member.styleClassName\\Label"},
                            "$member.name")
                        ),
                    TD({class: "memberValueCell"},
                        TAG("$member.tag", {object: "$member.value"})
                        )
                    );

            const SizerRow =
                TR(
                    TD({width: "40%"}),
                    TD({width: "60%"})
                    );

            const DirTablePlate = domplate(Firebug.Rep,
            {

                watchTag:
                    TABLE({class: "domTable", cellpadding: 0, cellspacing: 0,
                        _toggles: "$toggles", _domPanel: "$domPanel", onclick: "$onClick"},
                        TBODY(
                            SizerRow

                            )
                        ),

                tableTag:
                    TABLE({class: "domTable", cellpadding: 0, cellspacing: 0,
                        _toggles: "$toggles", _domPanel: "$domPanel", onclick: "$onClick"},
                        TBODY(
                            SizerRow
                            )
                        ),

            // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

                /**
                * The row that will be the parent of all the members that are about to be added.
                * Returning the FOR with a static rowRep is annoying; I'd like to return a FOR
                * that checks the member for the correct rowRep with which to represent itself.
                * Something like
                * 'FOR("member", "$member", "$member|getARowRep" ).
                * This works in that it calls the method (if it is defined globally) but for somereason there is a
                *  failure on 'getFirstChild'. I don't really get that.
                * I think it is a scope/timing issue; the FOR is compiled as it is created, WITH the correct rep.
                * Running with the 'getRowRep" method causes the rep to not be a compiled object.
                */
                getRowTag: function( row )
                {
                    var rowRep = RowTag;
                    if ( row )
                    {
                        var testFireObject;
                        if ( row.getAttribute("isTestFire") == 1 )
                        {
                            testFireObject = row.associatedObject;
                            rowRep = testFireObject.rowRep;
                        }
                    }
                    return FOR("member", "$members", rowRep);
                },

                selectedRow: null,

            /**
             * Handle the onClick event for a TestFireTreePanel Row object.
             *
             */
                onClick: function( event )
                {
                    var row = getAncestorByClass(event.target, "memberRow");
                    var panel = row.parentNode.parentNode.domPanel;
                    var toggles = row.parentNode.parentNode.toggles;

                    /* Ignore left (menu) clicks. */
                    if ( !isLeftClick(event) )
                        return;

                    var label = getAncestorByClass(event.target, "memberLabel");
                    if ( label && hasClass(row, "hasChildren") )
                    {
                        row = label.parentNode.parentNode;
                        this.toggleRow(row); /* Expand the tree item. */
                    }
                    else
                    {
                        var object = Firebug.getRepObject(event.target);
                        if ( typeof(object) == "function" )
                        {
                            FirebugChrome.select(object, "script");
                            cancelEvent(event);
                        }
                        else if ( event.detail == 2 && !object )
                        {
                            /* event.detail == 2 implies double click. This block is causing an edit of a property
                            * in the tree.
                            * For example, double click on a test name to trigger editing mode.
                            */
                            if ( panel )
                            {
                                var rowValue = panel.getRowPropertyValue(row);
                                if ( typeof(rowValue) == "boolean" )
                                    panel.setPropertyValue(row, !rowValue);
                                else
                                    panel.editProperty(row);
                                cancelEvent(event);
                            }
                        }
                    }
                },



            /**
             * Expand or collapse the passed row.
             * Collapsed rows lose all state (sub-rows are toggled as well).
             *
             * Currently uses the rowTag CONST to create each row; modifying this to retrieve a tag from the object.
             *
             * @row The row to expand or collapse.
             */
                toggleRow: function( row )
                {
                    try
                    {
                        var level = parseInt(row.getAttribute("level"));
                        var toggles = row.parentNode.parentNode.toggles;
                        var path;
                        var rowTag;
                        if ( hasClass(row, "opened") )
                        {
                            removeClass(row, "opened");

                            if ( toggles )
                            {
                                path = getPath(row);

                                // Remove the path from the toggle tree
                                for ( var i = 0; i < path.length; ++i )
                                {
                                    if ( i == path.length - 1 )
                                        delete toggles[path[i]];
                                    else
                                        toggles = toggles[path[i]];
                                }
                            }

                            rowTag = this.getRowTag(row);
                            var tbody = row.parentNode;

                            setTimeout(function()
                            {
                                for ( var firstRow = row.nextSibling; firstRow; firstRow = row.nextSibling )
                                {
                                    if ( parseInt(firstRow.getAttribute("level")) <= level )
                                        break;

                                    tbody.removeChild(firstRow);
                                }
                            }, row.insertTimeout ? row.insertTimeout : 0);
                        }
                        else
                        {
                            setClass(row, "opened");

                            if ( toggles )
                            {
                                path = getPath(row);

                                // Mark the path in the toggle tree
                                for ( var j = 0; j < path.length; ++j )
                                {
                                    var name = path[j];
                                    if ( name in toggles )
                                        toggles = toggles[name];
                                    else
                                        toggles = toggles[name] = {};
                                }
                            }

                            var containerCell = row.lastChild.firstChild;

                            var value = row.associatedObject ? row.associatedObject : containerCell.repObject;
                            var members = getMembers(value, level + 1, containerCell);

                            rowTag = this.getRowTag(row);
                            var lastRow = row;

                            var delay = 0;
                            while ( members.length )
                            {
                                // add up to INSERT_SLICE_SIZE elements at a time, with a slight delay between each batch.
                                setTimeout(function( slice, isLast )
                                {
                                    if ( lastRow.parentNode )
                                        lastRow = rowTag.insertRows({members: slice}, lastRow)[1];

                                    if ( isLast )
                                        delete row.insertTimeout;
                                }, delay, members.splice(0, INSERT_SLICE_SIZE), !members.length);

                                delay += insertInterval;
                            }

                            row.insertTimeout = delay;
                        }
                    }
                    catch( ex )
                    {
                        Firebug.TestFire.util.tf_debug(ex);
                    }

                }
            });

            const ToolboxPlate = domplate(
            {
                tag:
                    DIV({class: "watchToolbox", _domPanel: "$domPanel", onclick: "$onClick"},
                        IMG({class: "closeButton", src: "blank.gif"})
                        ),

                onClick: function( event )
                {
                    var toolbox = event.currentTarget;
//                    toolbox.domPanel.deleteWatch(toolbox.watchRow);
                    alert("Deleting of testsets not yet supported.");
                }
            });

            const UpDownPlate = domplate(
            {
                tag:
                    DIV({class: "watchToolbox", _domPanel: "$domPanel"},
                        IMG({class: "$upDisabled upButton", src: "blank.gif", onclick: "$onClickUp"}),
                        IMG({class: "$downDisabled downButton", src: "blank.gif", onclick: "$onClickDown"})
                        ),

                onClickUp: function( event )
                {
                    var upDownToolbox = event.currentTarget;
                    var testFireObj = upDownToolbox.parentNode.watchRow.associatedObject;
                    if(testFireObj.isUpAvailable())
                    {
                        testFireObj.moveUp();
                    }
                },

                onClickDown: function( event )
                {
                    var upDownToolbox = event.currentTarget;
                    var testFireObj = upDownToolbox.parentNode.watchRow.associatedObject;
                    if(testFireObj.isDownAvailable())
                    {
                        testFireObj.moveDown();
                    }
                }
            });

            // ************************************************************************************************

            TestFireTreePanelSupport = extend(Firebug.Panel,
            {
                tag: DirTablePlate.watchTag,
                getState: function()
                {
                    if ( !this.state )
                    {
                        this.state = {};
                    }
                    return state;
                },


                /**
                 * TargetNode is the node listed as the first item in the TreePanel.
                 * Fired by a ctrl+shift + right-click and from the right-click context menu.
                 *
                 */
                setTargetNode: function( obj )
                {
                    try
                    {
                        // this.removeWatch(this.getState().selectedTargetNodeWrapper);
                        this.getState().selectedTargetNodeWrapper =
                            new TestFireObjectWrapper(this, obj, "Selected Object", true, Firebug.TestFire);
                        this.setWatchValue(0, this.getState().selectedTargetNodeWrapper);
                        FirebugContext.getPanel("TestFireCodePanel").rebuild(this.getState().testset);
                        this.rebuild();
                    }
                    catch( e )
                    {
                        Firebug.TestFire.util.tf_debug(e);
                    }
                },


                /**
                * Add listeners to intercept the given events.
                * Called by the Firebug native code when the panel is loaded.
                */
                initializeNode: function( oldPanelNode )
                {
                    this.panelNode.addEventListener("mousedown", this.onMouseDown, false);
                    this.panelNode.addEventListener("mouseover", this.onMouseOver, false);
                    this.panelNode.addEventListener("mouseout", this.onMouseOut, false);
                },





                /**
                 *
                 * @param members
                 * @param update
                 * @param scrollTop
                 */
                showMembers: function( members, update, scrollTop )
                {
                    // "If we are still in the midst of inserting rows, cancel all pending
                    // insertions here - this is a big speedup when stepping in the debugger."
                    // May or may not do anything at all for TestFire. *shrug*.
                    if ( this.timeouts )
                    {
                        for ( var i = 0; i < this.timeouts.length; ++i )
                            this.context.clearTimeout(this.timeouts[i]);
                        delete this.timeouts;
                    }

                    if ( !members.length )
                        return this.showEmptyMembers();

                    var panelNode = this.panelNode;
                    var priorScrollTop = scrollTop == undefined ? panelNode.scrollTop : scrollTop;

                    // If we are asked to "update" the current view, then build the new table
                    // offscreen and swap it in when it's done
                    var offscreen = update && panelNode.firstChild;
                    var dest = offscreen ? this.document : panelNode;

                    var table = this.tag.replace({domPanel: this, toggles: this.getState().toggles}, dest);
                    var tbody = table.lastChild;
                    var rowTag = DirTablePlate.getRowTag(null);

                    // Insert the first slice immediately
                    var slice = members.splice(0, INSERT_SLICE_SIZE);
                    rowTag.insertRows({members: slice}, tbody.lastChild);

                    var timeouts = [];

                    var delay = 0;
                    while ( members.length )
                    {
                        timeouts.push(this.context.setTimeout(function( slice )
                        {
                            rowTag.insertRows({members: slice}, tbody.lastChild);

                            if ( (panelNode.scrollHeight + panelNode.offsetHeight) >= priorScrollTop )
                                panelNode.scrollTop = priorScrollTop;
                        }, delay, members.splice(0, INSERT_SLICE_SIZE)));

                        delay += insertInterval;
                    }

                    if ( offscreen )
                    {
                        timeouts.push(this.context.setTimeout(function()
                        {
                            if ( panelNode.firstChild )
                                panelNode.replaceChild(table, panelNode.firstChild);
                            else
                                panelNode.appendChild(table);

                            // Scroll back to where we were before
                            panelNode.scrollTop = priorScrollTop;
                        }, delay));
                    }
                    else
                    {
                        timeouts.push(this.context.setTimeout(function()
                        {
                            panelNode.scrollTop = scrollTop == undefined ? 0 : scrollTop;
                        }, delay));
                    }

                    this.timeouts = [];
                },

                showEmptyMembers: function()
                {
                    this.tag.replace({domPanel: this, toggles: {}}, this.panelNode);
                },

                findPathObject: function( object )
                {
                    var pathIndex = -1;
                    for ( var i = 0; i < this.getState().objectPath.length; ++i )
                    {
                        if ( this.getPathObject(i) == object )
                            return i;
                    }

                    return -1;
                },

                getPathObject: function( index )
                {
                    var object = this.getState().objectPath[index];
                    if ( object instanceof Property )
                        return object.getObject();
                    else
                        return object;
                },

                getRowObject: function( row )
                {
                    var object = getRowOwnerObject(row);
                    return object ? object : this.selection;
                },

                getRowPropertyValue: function( row )
                {
                    var object = this.getRowObject(row);
                    if ( object )
                    {
                        var propName = getRowName(row);

                        if ( object instanceof jsdIStackFrame )
                            return Firebug.Debugger.evaluate(propName, this.context);
                        else if ( object[propName] )
                        {
                            return object[propName];
                        }
                        else
                        {
                            return row.associatedObject;
                        }
                    }
                },

                copyProperty: function( row )
                {
                    var value = this.getRowPropertyValue(row);
                    copyToClipboard(value);
                },

                /* Fired on table-row by either a double click or the choice of 'edit property' from the context
                * menu.*/
                editProperty: function( row, editValue )
                {
                    try
                    {
                        if ( hasClass(row, "watchRow") )
                        {
                            return undefined;
                            // no editing the TestSet
                        }
                        else
                        {
                            var object = this.getRowObject(row);

                            this.context.thisValue = object;
                            var doEdit = true;
                            // always edit for primatives.

                            if ( !editValue )
                            {
                                var propValue = this.getRowPropertyValue(row);

                                var type = typeof(propValue);
                                if ( type == "undefined" || type == "number" || type == "boolean" )
                                    editValue = propValue;
                                else if ( type == "string" )
                                    editValue = "\"" + escapeJS(propValue) + "\"";
                                else if ( propValue == null )
                                    editValue = "null";
                                else if ( object instanceof Window || object instanceof jsdIStackFrame )
                                    editValue = getRowName(row);
                                else
                                {
                                    doEdit = editValue && 'editable' in editValue && !editValue.editable;
                                    editValue = "this." + getRowName(row);
                                }
                            }
                            if ( !doEdit )
                            {
                                return;
                            }

                            Firebug.Editor.startEditing(row, editValue);
                        }
                    }
                    catch( ex )
                    {
                        Firebug.TestFire.util.tf_debug(ex);
                    }
                },

                deleteProperty: function( row )
                {

                    var object = getRowOwnerObject(row);
                    if ( !object )
                    {
                        object = this.selection;
                    }

                    if ( object )
                    {
                        var name = getRowName(row);
                        try
                        {
                            delete object[name];
                        }
                        catch ( exc )
                        {
                            return;
                        }

                        this.rebuild(true);
                        this.markChange();
                    }

                },

                setPropertyValue: function( row, value )
                {
                    var name = getRowName(row);
                    if ( name == "this" )
                        return;

                    var object = this.getRowObject(row);
                    if ( object && !(object instanceof jsdIStackFrame) )
                    {
                        var val;
                        try
                        {
                            val = Firebug.CommandLine.evaluate(value, this.context, null, object);
                        }
                        catch ( exc )
                        {
                            try
                            {
                                // If the value doesn't parse, then just store it as a string.  Some users will
                                // not realize they're supposed to enter a JavaScript expression and just type
                                // literal text
                                val = value;
                            }
                            catch ( exc )
                            {
                                return;
                            }
                        }
                        var setter = 'set' + name.charAt(0).toUpperCase() + name.substring(1);
                        if(object[setter])
                        {
                            object[setter](val);
                        }
                        else{
                            object[name] = val;
                        }
                    }
                    else if ( this.context.stopped )
                    {
                        try
                        {
                            Firebug.CommandLine.evaluate(name + "=" + value, this.context);
                        }
                        catch ( exc )
                        {
                            try
                            {
                                // See catch block above...
                                object[name] = value;
                            }
                            catch ( exc )
                            {
                                return;
                            }
                        }
                    }

                    this.rebuild(true);
                    this.markChange();
                },

                highlightRow: function( row )
                {
                    if ( this.highlightedRow )
                        cancelClassTimed(this.highlightedRow, "jumpHighlight", this.context);

                    this.highlightedRow = row;

                    if ( row )
                        setClassTimed(row, "jumpHighlight", this.context);
                },

                addWatch: function( expression )
                {
                    if ( !this.getState().watches )
                        this.getState().watches = [];

                    this.getState().watches.push(expression);
                    this.rebuild(true);
                },

                removeWatch: function( expression )
                {
                    if ( !this.getState().watches )
                        return;

                    var index = this.getState().watches.indexOf(expression);
                    if ( index != -1 )
                        this.getState().watches.splice(index, 1);
                },

                setWatchValue: function( row, value )
                {
                    try
                    {
                        var rowIndex;
                        if ( typeof row == 'number' )
                        {
                            rowIndex = row;
                        }
                        else
                        {
                            rowIndex = getWatchRowIndex(row);
                        }
                        this.getState().watches[rowIndex] = value;
                        this.rebuild(true);
                    }
                    catch( ex )
                    {
                        Firebug.TestFire.util.tf_debug(ex);
                    }

                },

                deleteWatch: function( row )
                {
                    var rowIndex = getWatchRowIndex(row);
                    this.getState().watches.splice(rowIndex, 1);
                    this.rebuild(true);

                    this.context.setTimeout(bindFixed(function()
                    {
                        this.showToolbox(null);
                    }, this));
                },

            // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

                showToolbox: function( row )
                {
                    var toolbox = this.getToolbox();
                    if ( row )
                    {
                        if ( hasClass(row, "editing") )
                            return;

                        toolbox.watchRow = row;

                        var offset = getClientOffset(row);
                        toolbox.style.top = offset.y + "px";
                        this.panelNode.appendChild(toolbox);
                    }
                    else
                    {
                        delete toolbox.watchRow;
                        if ( toolbox.parentNode )
                            toolbox.parentNode.removeChild(toolbox);
                    }
                },

                getToolbox: function()
                {
                    if ( !this.toolbox )
                        this.toolbox = ToolboxPlate.tag.replace({domPanel: this}, this.document);

                    return this.toolbox;
                },

                showUpDownToolbox: function( row )
                {
                    var toolbox = this.getUpDownToolbox(row);
                    if ( row )
                    {
                        if ( hasClass(row, "editing") )
                            return;

                        toolbox.watchRow = row;

                        var offset = getClientOffset(row);
                        toolbox.style.top = offset.y + "px";
                        this.panelNode.appendChild(toolbox);
                    }
                    else
                    {
                        delete toolbox.watchRow;
                        if ( toolbox.parentNode )
                            toolbox.parentNode.removeChild(toolbox);
                        this.upDownToolbox = null;

                    }
                },

                getUpDownToolbox: function(member)
                {
                    try{
                        if ( !this.upDownToolbox )
                        {
                            this.upDownToolbox = UpDownPlate.tag.replace(
                            {
                                domPanel: this,
                                upDisabled: (member && member.associatedObject.isUpAvailable()) ? '' : 'upDisabled',
                                downDisabled: (member && member.associatedObject.isDownAvailable() ) ? '' : 'downDisabled'
                            },
                            this.document);
                        }
                    }
                    catch( ex )
                    {
                        Firebug.TestFire.util.tf_debug(ex);
                    }

                    return this.upDownToolbox;
                },

            // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

                isLeftClick: function( event )
                {
                    return (((event.which) && (event.which == 1)) ||
                            ((event.button) && (event.button == 1)));
                },


                onMouseDown: function( event )
                {
                    if ( isLeftClick(event) )
                    {
                        var watchNewRow = getAncestorByClass(event.target, "watchNewRow");
                        if ( watchNewRow )
                        {
                            this.editProperty(watchNewRow);
                            cancelEvent(event);
                        }
                    }
                },

                onMouseOver: function( event )
                {
                    var upDownRow = getAncestorByClass(event.target, "upDownRow");
                    if ( upDownRow )
                    {
                        this.showUpDownToolbox(upDownRow);
                        return;
                    }
                    var watchRow = getAncestorByClass(event.target, "watchRow");
                    if ( watchRow ) {
                        this.showToolbox(watchRow);
                    }

                },

                onMouseOut: function( event )
                {
                    var upDownRow = getAncestorByClass(event.target, "upDownRow");
                    upDownRow = upDownRow ? upDownRow : getAncestorByClass(event.relatedTarget, "upDownRow");

                    if ( isAncestor(event.relatedTarget, this.getToolbox())
                        || (upDownRow && isAncestor(event.relatedTarget, this.getUpDownToolbox() ) ))
                    {
                        return;
                    }

                    var watchRow = getAncestorByClass(event.relatedTarget, "watchRow");
                    if ( !watchRow )
                        this.showToolbox(null);
                    if ( upDownRow )
                        this.showUpDownToolbox(null);
                },


            // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
            // extends Panel






                supportsObject: function( object )
                {
                    return typeof(object) != "undefined";
                },

                select: function( object, forceUpdate )
                {
                    if ( !object || typeof(object) == "undefined" )
                    {
                        return;
                    }

                    var elPath = getElementXPath(object);
                    var doc = this.context.window.document;
                    var el = doc.evaluate(elPath, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);

                    el = el.singleNodeValue;
                    this.setTargetNode(el);
                },

                refresh: function()
                {
                    this.rebuild(true);
                },

            /**
             *
             */
                updateSelection: function(  )
                {
                    var frame = this.context.currentFrame;

                    var newFrame = frame && frame.script != this.lastScript;
                    if ( newFrame )
                    {
                        this.getState().toggles = {};
                        this.lastScript = frame.script;
                    }

                    var members = [];

                    if ( this.getState().watches )
                    {
                        for ( var i = 0; i < this.getState().watches.length; ++i )
                        {
                            var expr = this.getState().watches[i];

                            var expressionValue;
                            try
                            {
                                expressionValue = expr;
                                expr = expr.displayValue ? expr.displayValue : "Target object";
                            }
                            catch ( exc )
                            {
                                if ( exc instanceof this.context.window.Error )
                                    expressionValue = new ErrorCopy(exc.message);
                                else
                                    expressionValue = new ErrorCopy(exc + "");
                            }

                            addMember("watch", members, expr, expressionValue, 0);
                        }
                    }
                    try
                    {
                        if ( frame )
                        {
                            var thisVar = frame.thisValue.getWrappedValue();
                            addMember("user", members, "this", thisVar, 0);

                            var listValue = {value: null}, lengthValue = {value: 0};
                            frame.scope.getProperties(listValue, lengthValue);

                            var props = [], funcs = [];
                            for ( var j = 0; j < lengthValue.value; ++j )
                            {
                                var prop = listValue.value[j];
                                var name = prop.name.getWrappedValue();
                                if ( name in ignoreVars )
                                    continue;

                                var value = prop.value.getWrappedValue();
                                if ( typeof(value) == "function" )
                                    addMember("userFunction", funcs, name, value, 0);
                                else
                                    addMember("user", props, name, value, 0);
                            }
                            members.push.apply(members, props);
                            members.push.apply(members, funcs);
                        }
                    }
                    catch( e )
                    {
                        Firebug.TestFire.util.tf_debug(e);
                    }
                    try
                    {
                        expandMembers(members, this.getState().toggles, 0, 0);
                        this.showMembers(members, !newFrame);
                    }
                    catch( e )
                    {
                        Firebug.TestFire.util.tf_debug(e);
                    }
                },

                getObjectPath: function( object )
                {
                    return this.getState().objectPath;
                },

                getDefaultSelection: function()
                {
                    return this.context.window;
                },

                updateOption: function( name, value )
                {
                    const optionMap = {showUserProps: 1, showUserFuncs: 1, showDOMProps: 1,
                        showDOMFuncs: 1, showDOMConstants: 1};
                    if ( name in optionMap )
                        this.rebuild(true);
                },

                getOptionsMenuItems: function()
                {
                    return [
                        optionMenu("ShowUserProps", "showUserProps"),
                        optionMenu("ShowUserFuncs", "showUserFuncs"),
                        optionMenu("ShowDOMProps", "showDOMProps"),
                        optionMenu("ShowDOMFuncs", "showDOMFuncs"),
                        optionMenu("ShowDOMConstants", "showDOMConstants"),
                        "-",
                    {label: "Refresh", command: bindFixed(this.rebuild, this, true) }
                        ];
                },

                /**
                 * Right click context menu items for each row.
                 * @param object The object, such as a TestSet.
                 * @param target Same as the Object?
                 */
                getContextMenuItems: function( object, target )
                {
                    var row = getAncestorByClass(target, "memberRow");

                    var items = [];

                    if ( row )
                    {
                        var rowName = getRowName(row);
                        var rowObject = this.getRowObject(row);
                        var rowValue = this.getRowPropertyValue(row);

                        var isTestFire = row.associatedObject && row.associatedObject.isTestFireObject;
                        var isStackFrame = rowObject instanceof jsdIStackFrame;

                        if ( typeof(rowValue) == "string" || typeof(rowValue) == "number" )
                        {
                            // Functions already have a copy item in their context menu
                            items.push(
                                "-",
                            {label: "CopyValue", command: bindFixed(this.copyProperty, this, row) });
                            items.push(
                                "-",
                            {label: isStackFrame ? "EditVariable" : "EditProperty",
                                command: bindFixed(this.editProperty, this, row) });

                            if ( !isStackFrame && !isDOMMember(rowObject, rowName) )
                            {
                                items.push({label: "DeleteProperty", command: bindFixed(this.deleteProperty, this, row) });
                            }
                        }
                        if ( isTestFire )
                        {
                            items = items.concat(row.associatedObject.getContextMenuItems());
                        }
                    }

                    items.push(
                        "-",
                    {label: "Refresh", command: bindFixed(this.rebuild, this, true) }
                        );

                    return items;
                },

                getEditor: function( target, value )
                {
                    if ( !this.editor )
                        this.editor = new DOMEditor(this.document);

                    return this.editor;
                }
            });

            // ************************************************************************************************



            // ************************************************************************************************

            // ************************************************************************************************



            // ************************************************************************************************
            // Local Helpers

            function DOMEditor( doc )
            {
                this.box = this.tag.replace({}, doc, this);
                this.input = this.box;

                this.tabNavigation = false;
                this.tabCompletion = true;
                this.completeAsYouType = false;
                this.fixedWidth = true;

                this.autoCompleter = Firebug.CommandLine.autoCompleter;
            }

            DOMEditor.prototype = domplate(Firebug.InlineEditor.prototype,
            {
                tag: INPUT({class: "fixedWidthEditor", type: "text",
                    oninput: "$onInput", onkeypress: "$onKeyPress"}),

                endEditing: function( target, value, cancel )
                {
                    // XXXjoe Kind of hackish - fix me
                    delete this.panel.context.thisValue;

                    if ( cancel || value == "" )
                        return;

                    var row = getAncestorByClass(target, "memberRow");
                    if ( !row )
                        this.panel.addWatch(value);
                    else if ( hasClass(row, "watchRow") )
                        this.panel.setWatchValue(row, value);
                    else
                        this.panel.setPropertyValue(row, value);
                }
            });

            // ************************************************************************************************
            // Local Helpers

            /**
             * 
             * @param object
             * @param level
             */
            function getMembers( object, level )
            {
                try
                {
                    if ( !level )
                        level = 0;

                    var memberStore = { testFire: [], ordinals : [], userProps : [], userClasses : [], userFuncs : [],
                        domProps : [], domFuncs : [], domConstants : []
                    }

                    try
                    {
                        memberStore.domMembers = getDOMMembers(object);
                        if ( level == 0 && this.currentTargetObject )
                        {
                            doAddMember(this, "currentTargetObject", memberStore, level, this.currentTargetObject);
                        }

                        if ( object.isTestFireObject )
                        {
                            // passes a function pointer.
                            object.getMembers(memberStore, level, doAddMember);
                        }
                        else
                        {
                            for ( var name in object )
                            {
                                doAddMember(object, name, memberStore, level, object[name]);
                            }
                        }
                    }
                    catch ( exc )
                    {
                        // Sometimes we get exceptions just from trying to iterate the members
                        // of certain objects, like StorageList, but don't let that gum up the works
                        //throw exc;
                        Firebug.TestFire.util.tf_debug(exc);
                    }

                    var members = [];

                    members.push.apply(members, memberStore.testFire);
                    members.push.apply(members, memberStore.ordinals);

                    if ( Firebug.showUserProps )
                    {
                        members.push.apply(members, memberStore.userProps);
                    }

                    if ( Firebug.showUserFuncs )
                    {
                        members.push.apply(members, memberStore.userClasses);

                        members.push.apply(members, memberStore.userFuncs);
                    }

                    if ( Firebug.showDOMProps )
                    {
                        members.push.apply(members, memberStore.domProps);
                    }

                    if ( Firebug.showDOMFuncs )
                    {
                        members.push.apply(members, memberStore.domFuncs);
                    }

                    if ( Firebug.showDOMConstants )
                        members.push.apply(members, memberStore.domConstants);
                }
                catch( e )
                {
                    Firebug.TestFire.util.tf_debug(e);
                }

                return members;
            }

            /**
             * Add child to row.
             * @param object The target object; the object which is the parent of the member items.
             * @param displayName The name e.g. 'setUp', 'Test Set'. Often the type of item, displayed on the left.
             * @param ops
             * @param level Depth of the tree item
             * @param value The display text, usually linked, on the right hand side of the row.
             */
            function doAddMember( object, displayName, ops, level, value )
            {
                try
                {
                    if ( displayName in ignoreVars )
                    {
                        return;
                    }

                    var ordinal = parseInt(displayName);
                    if(object.isTestFireObject)
                    {
                        addMember("testFire", ops.ordinals, displayName, value, level);
                    }
                    else if ( ordinal || ordinal == 0 )
                    {
                        addMember("ordinal", ops.ordinals, displayName, value, level);
                    }
                    else if ( typeof(value) == "function" )
                    {
                        if ( isClassFunction(value) )
                            addMember("userClass", ops.userClasses, displayName, value, level);
                        else if ( displayName in ops.domMembers )
                            addMember("domFunction", ops.domFuncs, displayName, value, level, ops.domMembers[displayName]);
                        else
                            addMember("userFunction", ops.userFuncs, displayName, value, level);
                    }
                    else
                    {
                        if ( displayName in ops.domMembers )
                            addMember("dom", ops.domProps, displayName, value, level, ops.domMembers[displayName]);
                        else if ( displayName in domConstantMap )
                            addMember("dom", ops.domConstants, displayName, value, level);
                        else
                            addMember("user", ops.userProps, displayName, value, level);
                    }
                }
                catch ( exc )
                {
                    // Sometimes we get exceptions trying to access certain members
                    Firebug.TestFire.util.tf_debug(exc);
                }
            }

            function expandMembers( members, toggles, offset, level )
            {
                var expanded = 0;

                for ( var i = offset; i < members.length; ++i )
                {
                    var member = members[i];
                    if ( member.level > level )
                        break;

                    if ( member.name in toggles )
                    {
                        member.open = "opened";

                        var newMembers = getMembers(member.value, level + 1);

                        var args = [i + 1, 0];
                        args.push.apply(args, newMembers);
                        members.splice.apply(members, args);

                        expanded += newMembers.length;
                        i += newMembers.length + expandMembers(members, toggles[member.name], i + 1, level + 1);
                    }
                }

                return expanded;
            }

            // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

            function isClassFunction( fn )
            {
                try
                {
                    for ( var name in fn.prototype )
                        return true;
                }
                catch ( exc )
                {
                }
                return false;
            }

            function hasProperties( ob )
            {
                try
                {
                    for ( var name in ob )
                        return true;
                }
                catch ( exc )
                {
                }
                return false;
            }

            /**
             *
             * @param type One of { 'watch', 'user', 'userFunction', 'ordinal', 'userClass', 'domFunction', 'dom' }
             *
             * @param props
             * @param name
             * @param value
             * @param level
             * @param order
             */
            function addMember( styleClassName, props, name, value, level, order )
            {
                var member;
                var associatedObject = value;

                try
                {

                    var isTestFire = value && value.isTestFireObject;
                    var rep;
                    var tag;

                    if ( isTestFire && value instanceof TestFireObjectWrapper )
                    {
                        name = value.name;
                        value = value.wrappedObject;

                    }
                    else if ( isTestFire )
                    {

                        rep = Firebug.getRep(value);
                        tag = rep.shortTag ? rep.shortTag : rep.tag;
                        member = value.getMember(styleClassName, props, name, level, order, tag);

                    }

                    if ( !member )
                    {
                        var valueType = typeof(value);
                        rep = Firebug.getRep(value);
                        tag = rep.shortTag ? rep.shortTag : rep.tag;
                        var hasChildren = (hasProperties(value) && !(value instanceof ErrorCopy) &&
                                           (valueType == "function" || (valueType == "object" && value != null)
                                               || (valueType == "string" && value.length > Firebug.stringCropLength)));

                        member = {
                            name: name,
                            value: value,
                            styleClassName: styleClassName,
                            rowClass: "memberRow-" + styleClassName,
                            open: "",
                            order: order,
                            level: level,
                            indent: level * 16,
                            hasChildren: hasChildren,
                            isTestFire : 0,
                            tag: tag,
                            associatedObject: associatedObject
                        };
                    }

                    if ( props )
                        props.push(member);
                }
                catch( e )
                {
                    Firebug.TestFire.util.tf_debug(e)
                }
                return member;
            }

            function getWatchRowIndex( row )
            {
                var index = -1;
                for ( ; row && hasClass(row, "watchRow"); row = row.previousSibling )
                    ++index;
                return index;
            }

            function getRowName( row )
            {
                return row.firstChild.textContent;
            }

            function getRowValue( row )
            {
                return row.lastChild.firstChild.repObject;
            }

            function getRowOwnerObject( row )
            {
                var parentRow = getParentRow(row);
                var ret;
                if ( parentRow )
                {
                    ret = getRowValue(parentRow);
                }
                return ret;
            }

            function getParentRow( row )
            {
                var level = parseInt(row.getAttribute("level")) - 1;
                for ( row = row.previousSibling; row; row = row.previousSibling )
                {
                    if ( parseInt(row.getAttribute("level")) == level )
                        return row;
                }
                return null;
            }

            function getPath( row )
            {
                var name = getRowName(row);
                var path = [name];

                var level = parseInt(row.getAttribute("level")) - 1;
                for ( row = row.previousSibling; row; row = row.previousSibling )
                {
                    if ( parseInt(row.getAttribute("level")) == level )
                    {
                        name = getRowName(row);
                        path.splice(0, 0, name);
                        --level;
                    }
                }
                return path;
            }

        }
        catch( e )
        {
            Firebug.TestFire.util.tf_debug(e);
        }

    }
});
